#!/usr/local/bin/perl -w
#

$ARGV[0] || die "usage:\n   vtags.pl [-r] [-f file_of_file_names] [-y lib_dir] [file] ...\n" ;

$file_cnt = 0 ;
my @inc_dirs = ();
my @inc_files = ();
my @missing_includes = ();
my $file_types = "[svV][vtT]*";    # allow either .v or .vt files

# These directories are NOT to be searched when using the -r option
my %exclude_dirs = ("mti", 0, "xilinx", 0);

sub read_lib_dir {
    my $lib_dir_name = shift ;
    my $type = shift;

    print "including $type directory $lib_dir_name\n" ;

    opendir (LIBDIR, $lib_dir_name) || die "can't open $type $lib_dir_name\n" ;
    my @lib_files = readdir(LIBDIR) ;
    foreach $lib_file (@lib_files) {
        push(@files, "$lib_dir_name/$lib_file") if ($lib_file =~ /\.v$/) ;
    }
    closedir (LIBDIR) ;
}


# look for the include file in our path....
# this includes our current directory as well as the include paths
# called with the include file name to look for as well as the calling
# file  path
sub do_include {
    my $ifile = shift;
    my $call_file = shift;
    my $call_dir = $call_file;
    $call_dir =~ s,/\w[\w.]+$,, ;
    my $ifile2 = $ifile;
    $ifile2 =~ s,^.*/,,;    # remove any leading PATH info

        my $found = "";
    foreach my $dir (($call_dir, @inc_dirs)) {
        if (-r "$dir/$ifile") {
            $found = "$dir/$ifile";
            last;
        } elsif (-r "$dir/$ifile2") {
            $found = "$dir/$ifile2";
            last;
        }
    }
    if ($found ne "") {
# print "do_include: adding $found\n";
        push (@inc_files, $found);
# also add the basename of the file as a tag JIC the include
# file doesn't have any modules
        $ifile2 =~ s/\.\S+$//;
        push (@tags, "$ifile2\t$found\t1");
# print "adding include tag entry $ifile2\t$found\t1\n";
    } else {
        push (@missing_includes, $ifile);
    }
}

# check a line for interesting things to process:
#    function, task, module, `include
sub proc_line {
# get the input line and line number
    $_ = shift;
    my $line_num = shift;

# print "Trying $_\n";
    if (/^\s*`include\s+"(\S+)"/) {
# look for the include file in our path....
        &do_include ($1, $file);
    } else {
# remove strings
        s,\\.,,g;    # no \ style escapes...
            s,"[^"]*",,g;    # remove matched "
            s,'[^']*',,g;    # remove matched '

# check for keywords, remember them
            if (/\b(module|task)\b/) {
                while (!eof(INPUT)) {
                    if (/\b(module|task)\s+([a-zA-Z]\w+)/) {
# print "Got $1 $2\n";
                        push (@tags, "$2\t$file\t$line_num");
                        last;
                    }
# task/module name must be on next line....
                    $_ .= <INPUT>;
                }
            } elsif (/\bfunction\b/) {
                while (!eof(INPUT)) {
                    if (/\bfunction(\s*\[\s*\S+\s*:\s*\S+\s*]\s*|\s+real\s+|\s+integer\s+|\s+)([a-zA-Z]\w*)/) {
#print "Got function $2\n";
                        push (@tags, "$2\t$file\t$line_num");
                        last;
                    }
# function name must be on next line....
                    $_ .= <INPUT>;
                }
            }
    }
}

# read all the files scanning for module/task/function keywords
sub scan_files {
    my (@files) = @_;

    $last_file = "";
    foreach $file (sort(@files)) {
# skip duplicate file names
        next if ($last_file eq $file);
        $last_file = $file;

        open (INPUT, $file) || (warn "can't open verilog file $file\n", next) ;
        $file_cnt++ ;
        $in_comment = 0 ;
# print "processing $file\n";
        while (<INPUT>) {
# Scan each line for keywords while keeping track of comments
# This can be written much more concisely, but this tries to
# minimize the code for each common case
            if (!$in_comment) {
# not currently in a block comment
                if (m,/[/*],) {
# some kind of start of comment
s,//.*$|/\*.*\*/,, ;    # strip //....  and /*....*/  comments
                    $in_comment = s,/\*.*$,, ;    # remove /*...$ comment text
                                                           }
                                                           &proc_line ($_, $.) if (/\b(include|module|task|function)\b/);
                                                           } else {
# we're in a comment already
if ( !($in_comment = !s,^.*\*/,,) ) {
    s,//.*$|/\*.*\*/,, ;    # strip //....  and /*....*/  comments
        $in_comment = s,/\*.*$,, ;    # remove /*...$ comment text
                                               &proc_line ($_, $.) if (/\b(include|module|task|function)\b/);
                                               }
                                               }
                                               }
                                               close (INPUT) ;
                                               }
                                               }

# look through current and lower directories looking for Verilog files
sub find_files {
my $root_dir = shift;
my $lib_file;

print "checking dir '$root_dir' ...\n";
opendir (LIBDIR, $root_dir) || die "can't open $root_dir\n" ;
my @lib_files = readdir(LIBDIR) ;
closedir (LIBDIR) ;
foreach $lib_file (@lib_files) {
$_ = $lib_file;
# skip . and .. entries!
next if (/^\.\.*$/);

if (-d "$root_dir/$lib_file") {
# recurse into a lower directory unless its in the exclude list
&find_files("$root_dir/$lib_file") unless (exists($exclude_dirs{$lib_file}));
} elsif (/\.$file_types$/) {
# print "adding file $root_dir/$lib_file\n";
push(@files, "$root_dir/$lib_file")
}
}
}

# expand command line to remove '-f' flags
while ($n_arg = shift) {
if ($n_arg eq "-f") {
# process the list of files
$n_arg = shift ;
open(FILES, $n_arg) || die "can't open cmd line file $n_arg\n" ;
while (<FILES>) {
chomp ;
if (/^-y\s+(.*$)/) {
# we got a -y argument specifying a library directory.
# the command line must have had quotes, like:
#    "-y my_dir"
read_lib_dir ($1, "lib_dir") ;
} elsif (/\+incdir\+(\S+)/) {
push (@inc_dirs, $1) ;
} else {
s/\s*$// ;            # remove trailing blanks
s/^.* // ;            # remove any leading -flags
/\.$file_types\s*$/ && push(@files, $_) ;    # save only .v files
}
}
} elsif ($n_arg =~ /^-y\s+(.*$)/) {
# we got a -y argument specifying a library directory.
# the command line must have had quotes, like:
#    "-y my_dir"
read_lib_dir ($1, "lib_dir") ;
} elsif ($n_arg eq "-y") {
# we got a -y argument specifying a library directory.
# the command line must NOT have had quotes, like:
#    -y my_dir
read_lib_dir (shift, "lib_dir") ;
} elsif ($n_arg eq "-r") {
&find_files(".");
} elsif ($n_arg =~ /\.$file_types\s*$/ ) {
push (@files, $n_arg) ;    # save .v files from cmd line
}
}

# print "all Verilog input files are: @files\n" ;

# scan all the specified input files for modules, tasks, functions, `includes
#   when done, we'll have added tags and created two arrays for `includes:
#   one if for includes in the same directory as the calling file or in the
#   include path, the other for missing includes
        &scan_files (@files) ;


        print "looking for missing include files...\n" ;
# these were not found in the include_path or in the directory of the
# calling file
        foreach $file (@files) {
            $file =~ s,/[^/]+$,,;        # remove trailing file name to get path
                push (@dirs, $file);
        }

$last_miss = "";
foreach $missing (sort(@missing_includes)) {
# only include each missing file once!
    next if ($last_miss eq $missing);
    $last_miss = $missing;

    my $found = 0;
    $last_dir = "";
    foreach $dir (sort(@dirs)) {
        if ($last_dir ne $dir) {
            my $target = "$dir/$missing";
            if (-f $target) {
# print "adding include file $missing from $dir\n";
                push(@inc_files, $target);
                $found = 1;

# also add the basename of the file as a tag JIC the include
# file doesn't have any modules
                my $base_name = $missing;
                $base_name =~ s,^.*/,,;        # no leading PATH info
                    $base_name =~ s,\.\S+$,,;    # no trailing suffix
                    push (@tags, "$base_name\t$target\t1");
# print "adding missing include $base_name\t$target\t1\n";
            }
        }
        $last_dir = $dir;
    }
    if ($found == 0) {
        print "NOTE: couldn't find include file $missing\n";
    }
}
# now process the include files
&scan_files (@inc_files) ;


# send out the tags file
open(TAGS, ">tags") || die "can't open tags file\n" ;
my $tag_cnt = 0;
my $last_tag = "";
foreach $i (sort (@tags) ) {
    print TAGS "$i\n" if ($i ne $last_tag);
    $last_tag = $i;
    $tag_cnt++;
}
print "$file_cnt files scanned, $tag_cnt tags found\n" ;

